# Memory Puzzle
# By Al Sweigart al@inventwithpython.com
# http://inventwithpython.com/pygame
# Released under a "Simplified BSD" license

import random, pygame, sys
from pygame.locals import *

FPS = 30 # 每秒帧数，程序的一般速度
WINDOWWIDTH = 640 # 窗口宽度的像素大小
WINDOWHEIGHT = 480 # 窗口的高度以像素为单位
REVEALSPEED = 8 # 开/盖）盒子速度
BOXSIZE = 40 # 盒高度和宽度的像素
GAPSIZE = 10 # 像素之间的间隙大小
BOARDWIDTH = 5 # 图标的列数
BOARDHEIGHT = 4 # 图标行数
SHOWCOUNT = 10 # 开场动画每次显示的盒子个数
assert (BOARDWIDTH * BOARDHEIGHT) % 2 == 0, '需要有偶数个箱子来匹配配对。'
XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2) #左右两边预留空间的宽度
YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * (BOXSIZE + GAPSIZE))) / 2) #上下两边预留空间的高度

#            R    G    B
GRAY     = (100, 100, 100)
NAVYBLUE = ( 60,  60, 100)
WHITE    = (255, 255, 255)
RED      = (255,   0,   0)
GREEN    = (  0, 255,   0)
BLUE     = (  0,   0, 255)
YELLOW   = (255, 255,   0)
ORANGE   = (255, 128,   0)
PURPLE   = (255,   0, 255)
CYAN     = (  0, 255, 255)

BGCOLOR = NAVYBLUE #背景颜色 海军蓝
LIGHTBGCOLOR = GRAY  #闪烁 灰色
BOXCOLOR = WHITE # 盒子颜色 白
HIGHLIGHTCOLOR = BLUE #高亮 蓝

DONUT = 'donut'
SQUARE = 'square'
DIAMOND = 'diamond'
LINES = 'lines'
OVAL = 'oval'

ALLCOLORS = (RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, CYAN) #所有的颜色
ALLSHAPES = (DONUT, SQUARE, DIAMOND, LINES, OVAL) #所有的形状
assert len(ALLCOLORS) * len(ALLSHAPES) * 2 >= BOARDWIDTH * BOARDHEIGHT, "对于定义的形状/颜色的数量来说，板太大了。"

def main(): #主入口
    global FPSCLOCK, DISPLAYSURF
    pygame.init()
    FPSCLOCK = pygame.time.Clock()
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

    mousex = 0 # 用于存储鼠标事件的x坐标
    mousey = 0 # 用于存储鼠标事件的y坐标
    pygame.display.set_caption('Memory Game')

    mainBoard = getRandomizedBoard() # 随机生成图标放入跟盒子对应的排列组合里
    revealedBoxes = generateRevealedBoxesData(False) #初始化所有盒子为’盖上‘状态 [[False]*7,[False]*7...]

    firstSelection = None # 将第一个打开的盒子的(x，y)存储起来。

    DISPLAYSURF.fill(BGCOLOR) #设置背景颜色，在开场动画就显示，不然就是黑色，很丑
    startGameAnimation(mainBoard) #游戏开场动画

    while True: # 游戏主循环
        mouseClicked = False #鼠标被放开为True

        DISPLAYSURF.fill(BGCOLOR) #设置背景颜色
        drawBoard(mainBoard, revealedBoxes) # 绘制所有盒子初始（盖）状态
        # pygame.draw.lines(DISPLAYSURF,(222,0,0),True,((XMARGIN,YMARGIN),(WINDOWWIDTH-XMARGIN,YMARGIN),
        #                                             (WINDOWWIDTH-XMARGIN,WINDOWHEIGHT-YMARGIN),(XMARGIN,WINDOWHEIGHT-YMARGIN)),1)
        for event in pygame.event.get(): # 事件处理循环
            if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE):
                pygame.quit()
                sys.exit()
            elif event.type == MOUSEMOTION:
                mousex, mousey = event.pos #记录鼠标移动的位置
            elif event.type == MOUSEBUTTONUP:
                mousex, mousey = event.pos #记录鼠标放开的位置
                # print(event.button)
                mouseClicked = True #鼠标被放开

        boxx, boxy = getBoxAtPixel(mousex, mousey) #判断鼠标当前位置是否在盒子范围内，是就返回盒子坐标
        if boxx != None and boxy != None:
            # 鼠标现在已经在一个盒子内
            if not revealedBoxes[boxx][boxy]:
                # 盒子未被打开
                drawHighlightBox(boxx, boxy) #突出显示（围一层矩形轮廓）
            if not revealedBoxes[boxx][boxy] and mouseClicked:
                # 盒子未被打开 且此时 鼠标放开----打开盒子
                revealBoxesAnimation(mainBoard, [(boxx, boxy)]) # 打开盒子（循环列表打开）
                revealedBoxes[boxx][boxy] = True # 将此坐标代表的盒子设置为“打开”状态
                if firstSelection == None:
                    # 当前的盒子是第一个打开的盒子，就记录
                    firstSelection = (boxx, boxy)
                else:
                    # 当前的盒子是第二个盒子，就比较两者的颜色及形状
                    icon1shape, icon1color = getShapeAndColor(mainBoard, firstSelection[0], firstSelection[1]) # 获取盒子坐标代表的图标 颜色和形状
                    icon2shape, icon2color = getShapeAndColor(mainBoard, boxx, boxy) #同上

                    if icon1shape != icon2shape or icon1color != icon2color:
                        # 图标不匹配，再盖上选择的两个盒子
                        pygame.time.wait(1000) # 等待一秒
                        coverBoxesAnimation(mainBoard, [(firstSelection[0], firstSelection[1]), (boxx, boxy)]) #盖上盒子（循环列表盖）
                        revealedBoxes[firstSelection[0]][firstSelection[1]] = False # 更改盒子状态为‘盖上’
                        revealedBoxes[boxx][boxy] = False # 更改盒子状态为‘盖上’
                    else :
                        # 图标匹配，再看看是否已经通关
                        if hasWon(revealedBoxes):
                            gameWonAnimation(mainBoard) # 播放游戏通关动画
                            pygame.time.wait(2000) # 等待2秒

                            # 重新开始游戏
                            mainBoard = getRandomizedBoard() # 随机生成图标放入与盒子对应的排列组合里
                            revealedBoxes = generateRevealedBoxesData(False) #初始化所有盒子为’盖上‘状态

                            # 一秒钟时间展示所有的未显示的盒子
                            drawBoard(mainBoard, revealedBoxes)
                            pygame.display.update()
                            pygame.time.wait(1000)

                            # 重放启动游戏动画
                            startGameAnimation(mainBoard)

                    # 不管匹不匹配，重置firstSelection变量
                    firstSelection = None

        # 重新绘制屏幕，受下面第二行代码影响，等待时钟滴答响，也就是下一次循环
        pygame.display.update()
        FPSCLOCK.tick(FPS)


def generateRevealedBoxesData(val):
    revealedBoxes = [] # [[False]*7,[False]*7...]
    for i in range(BOARDWIDTH):
        revealedBoxes.append([val] * BOARDHEIGHT)
    return revealedBoxes

# 随机生成图标放入与盒子对应的排列组合里
def getRandomizedBoard():
    # 按顺序组合出颜色和形状的图标列表
    icons = []
    for color in ALLCOLORS:
        for shape in ALLSHAPES:
            icons.append( (shape, color) )

    random.shuffle(icons) # 打乱列表
    numIconsUsed = int(BOARDWIDTH * BOARDHEIGHT / 2) # 计算需要多少图标
    icons = icons[:numIconsUsed] * 2 #别忘了，每种图标存2个
    random.shuffle(icons) #拆散一对一对的

    # 将存入图标列表的图标放入显示时的排列组合里 [[第一行的图标],[第二...]...]
    board = []
    for x in range(BOARDWIDTH):
        column = []
        for y in range(BOARDHEIGHT):
            column.append(icons[0])
            del icons[0] #从图标列表里删除已放入的图标
        board.append(column)
    return board


def splitIntoGroupsOf(groupSize, theList):
    #将一个列表以groupSzie个为一组拆分成一个新列表作为元素添加到新总列表中返回，
    result = []
    for i in range(0, len(theList), groupSize):
        result.append(theList[i:i + groupSize]) #list[x:y],y超出索引不会报错，相当于到底
    return result

# 将盒子坐标转换为像素坐标
def leftTopCoordsOfBox(boxx, boxy):
    left = boxx * (BOXSIZE + GAPSIZE) + XMARGIN
    top = boxy * (BOXSIZE + GAPSIZE) + YMARGIN
    return (left, top)


def getBoxAtPixel(x, y):
    for boxx in range(BOARDWIDTH):
        for boxy in range(BOARDHEIGHT):
            left, top = leftTopCoordsOfBox(boxx, boxy) #根据盒子坐标获取像素坐标
            boxRect = pygame.Rect(left, top, BOXSIZE, BOXSIZE) #绘制盒子矩形
            if boxRect.collidepoint(x, y): #检测鼠标当前点是否在哪个盒子内内，是就返回盒子坐标
                return (boxx, boxy)
    return (None, None)

# 开始绘制图标
def drawIcon(shape, color, boxx, boxy):
    quarter = int(BOXSIZE * 0.25) # 语法糖
    half  =   int(BOXSIZE * 0.5)  # 语法糖

    left, top = leftTopCoordsOfBox(boxx, boxy) # 板坐标转换成像素坐标
    # 画出形状描绘颜色
    if shape == DONUT: #绘制圆
        pygame.draw.circle(DISPLAYSURF, color, (left + half, top + half), half - GAPSIZE)
        # pygame.draw.circle(DISPLAYSURF, BGCOLOR, (left + half, top + half), quarter - GAPSIZE)
    elif shape == SQUARE: #矩形
        pygame.draw.rect(DISPLAYSURF, color, (left + quarter, top + quarter, BOXSIZE - half, BOXSIZE - half))
    elif shape == DIAMOND: #多边形
        pygame.draw.polygon(DISPLAYSURF, color, ((left + half, top), (left + BOXSIZE - 1, top + half), (left + half, top + BOXSIZE - 1), (left, top + half)))
    elif shape == LINES: #线
        for i in range(0, BOXSIZE, 4):
            pygame.draw.line(DISPLAYSURF, color, (left, top + i), (left + i, top))
            pygame.draw.line(DISPLAYSURF, color, (left + i, top + BOXSIZE - 1), (left + BOXSIZE - 1, top + i))
    elif shape == OVAL: #椭圆
        pygame.draw.ellipse(DISPLAYSURF, color, (left, top + quarter, BOXSIZE, half))

# 获取盒子坐标代表的图标 颜色和形状
def getShapeAndColor(board, boxx, boxy):
    # x y点的形状值存储在[x][y][0]
    # x y点的颜色值存储在[x][y][1]
    return board[boxx][boxy][0], board[boxx][boxy][1]

# 根据参数绘制盒子封面
def drawBoxCovers(board, boxes, coverage):
    # “board”是图标列表
    # boxes，每个盒子的位置[（x,y）]列表，已被打乱
    # coverage，覆盖范围
    for box in boxes:
        left, top = leftTopCoordsOfBox(box[0], box[1]) #将板坐标转换为像素坐标
        pygame.draw.rect(DISPLAYSURF, BGCOLOR, (left, top, BOXSIZE, BOXSIZE)) #绘制一个矩形
        shape, color = getShapeAndColor(board, box[0], box[1]) # 获取盒子坐标代表的图标 颜色和形状
        drawIcon(shape, color, box[0], box[1]) # 绘制图标
        if coverage > 0: # 只在正值时画封面（每次都会减少范围，形成翻盒子动画）
            pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top, coverage, BOXSIZE))
    pygame.display.update()
    FPSCLOCK.tick(FPS)

# 翻盒子动画（循环列表翻）
def revealBoxesAnimation(board, boxesToReveal):
    for coverage in range(BOXSIZE, (-REVEALSPEED) - 1, -REVEALSPEED):
        drawBoxCovers(board, boxesToReveal, coverage) #开始翻


# 盖盒子动画（循环列表盖）
def coverBoxesAnimation(board, boxesToCover):
    for coverage in range(0, BOXSIZE + REVEALSPEED, REVEALSPEED):
        drawBoxCovers(board, boxesToCover, coverage)

# 绘制盒子
def drawBoard(board, revealed):
    # 将所有的盒子都绘制出来，‘打开’状态绘制图标 ‘关闭’状态显示白框
    for boxx in range(BOARDWIDTH):
        for boxy in range(BOARDHEIGHT):
            left, top = leftTopCoordsOfBox(boxx, boxy) #将板坐标转换为像素坐标
            if not revealed[boxx][boxy]:
                # 根据像素坐标画出所有白框
                pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top, BOXSIZE, BOXSIZE))
            else:
                # 画出(显示)随机SHOWCOUNT个图标
                shape, color = getShapeAndColor(board, boxx, boxy) #取出形状和颜色
                drawIcon(shape, color, boxx, boxy) #绘制图标

# 鼠标停在的盒子位置进行额外的显示（width>0就是矩形轮廓）
def drawHighlightBox(boxx, boxy):
    left, top = leftTopCoordsOfBox(boxx, boxy) #根据盒子坐标计算像素坐标
    pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, (left - 5, top - 5, BOXSIZE + 10, BOXSIZE + 10), 4) #绘制额外的矩形显示


# 游戏开场动画
def startGameAnimation(board):#board 排列组合好的图标列表
    # 每次随机地显示SHOWCOUNT个盒子
    coveredBoxes = generateRevealedBoxesData(False) # 所有盒子初始化’盖上‘状态[[False]*7,[False]*7...]
    boxes = []
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            boxes.append( (x, y) )
    random.shuffle(boxes) # 打乱
    boxGroups = splitIntoGroupsOf(SHOWCOUNT, boxes) #SHOWCOUNT个盒子一组的列表为元素形成的列表

    drawBoard(board, coveredBoxes) #画出盒子初始白框，和显示的SHOWCOUNT个里的图标
    for boxGroup in boxGroups:
        revealBoxesAnimation(board, boxGroup) #绘制翻盒子动画
        coverBoxesAnimation(board, boxGroup) #绘制盖盒子动画

# 游戏胜利动画
def gameWonAnimation(board):
    # 当玩家获胜时，让背景颜色闪烁
    coveredBoxes = generateRevealedBoxesData(True) #让所有盒子为’打开‘状态，这里这样做是为了让赢的条件设置为更宽松的时候，例 匹配3对即算成功，成功时打开所有盒子
    color1 = LIGHTBGCOLOR #闪烁颜色1 亮色
    color2 = BGCOLOR #闪烁颜色2 背景色

    for i in range(13):
        color1, color2 = color2, color1 # 交换颜色
        DISPLAYSURF.fill(color1) #渲染背景色
        drawBoard(board, coveredBoxes) #根据盒子状态绘制所有盒子，当前胜利了，则都是绘制出图标
        pygame.display.update()
        pygame.time.wait(300) #等待0.3秒

# 判断是否通关游戏
def hasWon(revealedBoxes):
    # 如果所有的框都被显示了，否则返回True
    for i in revealedBoxes:
        if False in i:
            return False # 如果有任何箱子被盖住，返回False。
    return True


if __name__ == '__main__':
    main()